package com.squeekeek.util.qrcode.Core;

import java.util.ArrayList;
import java.util.List;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;

public class QRAlignmentPatternFinder {
	public static List <Point> getAlignmentPatterns(Mat inImg, int rowStart, int rowEnd, int colStart, int colEnd) {
		List <Point> centerPoints = new ArrayList <Point> ();
		
		centerPoints = scanImage(inImg, rowStart, rowEnd, colStart, colEnd);
		
		return centerPoints;
	}
	
	private static List <Point> scanImage(Mat inImg, int rowStart, int rowEnd, int colStart, int colEnd) {
		int pixVal;
		int centerX;
		int currentState = 0;
		int centerY;
		int[] statePixels = {0, 0, 0};
		Point foundAlignmentCenter;
		List <Point> centerPoints = new ArrayList <Point> ();
		
		// Scan rows for 1:1:1 ratio of W:B:W pixels. Let state 1 be the
		// state of counting the black pixels at the center of the alignment
		// pattern
		for(int i = rowStart; i < rowEnd; i += 3) {
			int j = colStart;
			
			// Remove black and white pixels
			while((j < colEnd) && ((int) inImg.get(i, j)[0] == QRBinarize.PIXEL_WHITE)) {
				j++;
			}
			
			while((j < colEnd) && ((int) inImg.get(i, j)[0] == QRBinarize.PIXEL_BLACK)) {
				j++;
			}
			
			for( ; j < colEnd; j++) {
				pixVal = (int) inImg.get(i, j)[0];
				
				if((currentState == 2) && (pixVal == QRBinarize.PIXEL_BLACK)) {
					centerX = (j - statePixels[2]) - (statePixels[1] / 2);

					// Cross-check with a vertical scan
					if(checkAlignmentRatio(statePixels)) {
						// Add prospective alignment pattern
						centerY = scanVertical(inImg, statePixels, i, centerX);
						
						if(centerY != -1) {
							foundAlignmentCenter = new Point(centerX, centerY);
							Core.circle(inImg, foundAlignmentCenter, 3, new Scalar(0x90), -1);
							
							if(isUniqueCenter(centerPoints, foundAlignmentCenter)) {
								centerPoints.add(foundAlignmentCenter);
							}
							
							// Revert to state 1 since we are already counting black
							// pixels. This makes checkAlignmentRatio() return false
							currentState = 1;
							
							statePixels[0] = 0;
							statePixels[1] = 0;
							statePixels[2] = 0;
						}
					}
					else {
						// Revert to state 1 where we have just finished
						// counting the center black pixels
						currentState = 1;
						
						statePixels[0] = statePixels[2];
						statePixels[1] = 0;
						statePixels[2] = 0;
					}
				}
				else if((currentState == 0) && (pixVal == QRBinarize.PIXEL_BLACK)) {
					currentState++;
				}
				else if((currentState == 1 && (pixVal == QRBinarize.PIXEL_WHITE))) {
					currentState++;
				}
				
				statePixels[currentState]++;
			}
			
			currentState = 0;
			
			statePixels[0] = 0;
			statePixels[1] = 0;
			statePixels[2] = 0;
		}
		
		return centerPoints;
	}
	
	private static int scanVertical(Mat inImg, int[] horizState, int iCoord, int centerJ) {
		int i;
		int pixVal;
		int currentState;
		int startHeight, endHeight;
		int[] statePixels = {0, 0, 0};
		
		// Try to scan a W:B:W ratio. Let state 1 be the
		// state of counting the center black pixels
		
		// Scan upwards
		currentState = 1;
		i = iCoord;
		while(i >= 0) {
			pixVal = (int) inImg.get(i, centerJ)[0];
			
			if((currentState == 0) && (pixVal == QRBinarize.PIXEL_BLACK)) {
				break;
			}
			else if((currentState == 1) && (pixVal == QRBinarize.PIXEL_WHITE)) {
				currentState = 0;
			}
			
			statePixels[currentState]++;
			i--;
		}
		
		if((i < 0) || (Math.abs(horizState[0] - statePixels[0]) > 1)) {
			return -1;
		}
		
		startHeight = iCoord - statePixels[0] - statePixels[1];
		
		// Scan downwards
		currentState = 1;
		i = iCoord + 1;
		while(i < inImg.rows()) {
			pixVal = (int) inImg.get(i, centerJ)[0];
			
			if((currentState == 2) && (pixVal == QRBinarize.PIXEL_BLACK)) {
				break;
			}
			else if((currentState == 1) && (pixVal == QRBinarize.PIXEL_WHITE)) {
				currentState = 2;
			}
			
			statePixels[currentState]++;
			i++;
		}
		
		if((i >= inImg.rows()) || (Math.abs(horizState[2] - statePixels[2]) > 1)) {
			return -1;
		}
		
		endHeight = startHeight + statePixels[0] + statePixels[1] + statePixels[2];

		if(checkAlignmentRatio(statePixels)) {
			//System.out.println(startHeight + " " + endHeight + " " + (startHeight + (endHeight - startHeight) / 2));
			//Core.circle(inImg, new Point(centerJ, startHeight), 3, new Scalar(0x90));
			//Core.circle(inImg, new Point(centerJ, endHeight), 3, new Scalar(0x90));
			//Core.circle(inImg, new Point(centerJ, iCoord), 3, new Scalar(0xA0), -1);
			
			return startHeight + (endHeight - startHeight) / 2;
		}
		
		return -1;
	}
	
	private static boolean checkAlignmentRatio(int[] statePixels) {
		int totalPixels = 0;
		double modSize;
		double maxVar;
		
		for(int i : statePixels) {
			if(i < 1) {
				return false;
			}
			
			totalPixels += i;
		}
		
		modSize = totalPixels / 3.0;
		maxVar = modSize / 2.0;
		
		for(int i = 0; i < 3; i++) {
			if((int) (1000 * Math.abs(modSize - statePixels[i])) > (int) (1000 * maxVar)) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Checks whether a point is already included in a list of points
	 * <br><br>
	 * The function checks whether the list of <code>OpenCV.Point points</code>
	 * already contains the point <code>OpenCV.Point singlePoint</code> to a tolerance
	 * of 5 pixels.
	 * 
	 * @param points The list of points
	 * @param singlePoint A single point to check
	 * @return Returns <code>true</code> if the point is not yet found in the
	 * given list of points
	 */
	private static boolean isUniqueCenter(List <Point> points, Point singlePoint) {
		for(Point iPoint : points) {
			if((int) (Math.abs(iPoint.x - singlePoint.x)) < 5 && (int) (Math.abs(iPoint.y - singlePoint.y)) < 5) {
				return false;
			}
		}
		
		return true;
	}
}
